package com.fzk.server;

import com.fzk.controller.IndexController;
import com.fzk.controller.LoginController;
import com.fzk.core.FileStoreService;
import com.fzk.log.Logger;
import com.fzk.router.MyDefaultCtx;
import com.fzk.vo.Ack;
import com.fzk.vo.HttpResult;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultHttpContent;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.multipart.*;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author fzk
 * @datetime 2023-03-05 20:03:19
 */
public class UploadHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
    private final FileStoreService fileService;
    private FullHttpRequest req;
    private HttpPostRequestDecoder decoder;
    private static final HttpDataFactory factory =
            new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE); // Disk if size exceed

    static {
        DiskFileUpload.deleteOnExitTemporaryFile = true; // should delete file
        // on exit (in normal
        // exit)
        DiskFileUpload.baseDirectory = null; // system temp directory
        DiskAttribute.deleteOnExitTemporaryFile = true; // should delete file on
        // exit (in normal exit)
        DiskAttribute.baseDirectory = null; // system temp directory
    }

    public UploadHandler(FileStoreService fileService) {
        this.fileService = fileService;
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        reset();
        ctx.fireChannelInactive();
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
        // 校验请求合法性
        if (!LoginController.validateToken(new MyDefaultCtx("mocklogid", ctx), request)) {
            Logger.info("not login but upload file, has reject");
            // 重定向到登录页面
            HttpResponseUtil.sendReqRedirect(ctx, request, IndexController.loginPageUri);
            return;
        }
        this.req = request;
        HttpResult result;
        // 1.参数和文件解析
        List<FileUpload> fileUploads = new ArrayList<>(1);// 文件
        Map<String, String> attrs = new HashMap<>();// 参数
        if (this.decoder == null) {
            this.decoder = new HttpPostRequestDecoder(factory, request);
        }
        try {
            decoder.offer(new DefaultHttpContent(request.content()));
            while (decoder.hasNext()) {// 上传多个文件时这里将循环调用
                InterfaceHttpData data = decoder.next();
                if (data != null) {
                    if (data.getHttpDataType() == InterfaceHttpData.HttpDataType.Attribute) {
                        Attribute attr = (Attribute) data;
                        attrs.put(attr.getName(), attr.getValue());
                    } else if (data.getHttpDataType() == InterfaceHttpData.HttpDataType.FileUpload) {
                        FileUpload fileUpload = (FileUpload) data;
                        if (!fileUpload.isCompleted()) Logger.warning("file not complete?");
                        if (fileUpload.content().readableBytes() == 0) {// 空文件则忽略
                            Logger.warning("空文件? 可能是表单未选择文件却点击上传, 前端未拦截? 又或者切片上传时切错了?已经忽略");
                            continue;
                        }
                        fileUploads.add(fileUpload);
                    } else {
                        Logger.error(String.format("出现了不也该出现的类型但已忽略: %s", data.getHttpDataType()));
                    }
                }
            }
        } catch (HttpPostRequestDecoder.EndOfDataDecoderException e1) {// decoder.hasNext()结束会抛出异常
            Logger.debug(String.format("文件上传处理开始, ip: %s, path: %s", ctx.channel().remoteAddress(), request.uri()));
            if (attrs.containsKey("position")) { // ajax分片上传
                result = sliceUpload(fileUploads, attrs);
            } else {// 表单上传文件
                result = formUpload(fileUploads, attrs);
            }

            Logger.debug(String.format("文件上传处理结束, ip: %s, path: %s, result: %s", ctx.channel().remoteAddress(), request.uri(), result));

            // 写响应
            HttpResponseUtil.json(ctx, HttpUtil.isKeepAlive(request), result);
        } finally {
            reset();// 清除资源
        }
    }

    private HttpResult formUpload(List<FileUpload> fileUploads, Map<String, String> attrs) throws IOException {
        // 1.参数检查
        if (fileUploads.size() == 0) {
            return new HttpResult(400, "表单上传无文件?前端没拦截?", null);
        }
        String bucket = attrs.get("bucket");
        String relativePath = attrs.get("relativePath");
        if (checkHasNull(bucket, relativePath)) {
            return new HttpResult(HttpResponseStatus.BAD_REQUEST.code(), "参数错误bucket, relativePath", null);
        } else {
            ArrayList<String> accessUrls = new ArrayList<>(fileUploads.size());
            // 2.文件处理
            for (FileUpload fileUpload : fileUploads) {
                String filename = fileUpload.getFilename();// 表单上传可以直接获取文件名

                // 储存文件
                ByteBuf buf = fileUpload.getByteBuf();
                String key = fileService.fileStore(bucket, relativePath, filename, buf);
                // 注意：这个fileUpload.getByteBuf()
                // 如果数据存储在缓存文件，则将整个文件读取到UnPooledByteBuf的byte[]中，可以不释放(不会出现内存泄露)，也可以释放（此时调用不会报错）
                // 如果是数据存储在内存，将直接返回其持有的ByteBuf，此时释放后，之后的reset()方法里的decoder.cleanFiles()方法将报错
                // ReferenceCountUtil.removeCallback(buf);
                accessUrls.add(String.format("https://fzk-tx.top/fs/api/download?key=%s", key));
            }
            return HttpResult.success(accessUrls);
        }
    }

    private HttpResult sliceUpload(List<FileUpload> fileUploads, Map<String, String> attrs) throws IOException {
        // 1.参数检查
        String bucket = attrs.get("bucket");
        String relativePath = attrs.get("relativePath");
        String filename = attrs.get("filename");
        long position = Long.parseLong(attrs.getOrDefault("position", "-1"));
        long size = Long.parseLong(attrs.getOrDefault("size", "-1"));
        if (checkHasNull(bucket, relativePath, filename)) {
            return new HttpResult(400, "参数错误", null);
        }
        if (position < 0 || size < 0) {
            return new HttpResult(400, "参数错误", null);
        }
        // 检查是否有文件内容
        if (fileUploads.size() == 0) {
            return new HttpResult(400, "分片上传无文件?前端分片出错了?", null);
        }
        Object ack = null;
        Logger.debug(String.format("文件表单分片上传，filename: %s, position: %d", filename, position));

        // 2.2 处理文件
        for (FileUpload fileUpload : fileUploads) {
            // 分片存储
            //fileUpload.retain();// 传递给其它方法需增加引用数
            ByteBuf buf = fileUpload.getByteBuf();
            long next_ack = position + buf.readableBytes();
            String key = fileService.fileSliceStore(bucket, relativePath, filename, position, size, buf);
            position = next_ack;
            // 注意：这个fileUpload.getByteBuf()
            // 如果数据存储在缓存文件，则将整个文件读取到UnPooledByteBuf的byte[]中，可以不释放(不会出现内存泄露)，也可以释放（此时调用不会报错）
            // 如果是数据存储在内存，将直接返回其持有的ByteBuf，此时释放后，之后的reset()方法里的decoder.cleanFiles()方法将报错
            // ReferenceCountUtil.removeCallback(buf);
            ack = new Ack(position, key);
        }

        return HttpResult.success(ack);
    }

    private void reset() {
        if (decoder != null) {
            // destroy the decoder to removeCallback all resources
            decoder.destroy();
            decoder = null;
        }
    }

    private boolean checkHasNull(String... arr) {
        for (String str : arr) {
            if (str == null || str.length() == 0) {
                return true;
            }
        }
        return false;
    }


    @Override// 错误拦截
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable t) {
        reset();
        t.printStackTrace();
        Logger.error(t.getMessage());
        if (ctx.channel().isActive()) {
            HttpResponseUtil.sendError(ctx, HttpUtil.isKeepAlive(req), HttpResponseStatus.INTERNAL_SERVER_ERROR);
        }
    }
}
